/******************************************************************************
	Cobalt - utility libraries for building XNA games
	Copyright (C) 2008 Philip Djonov

	This program is free software; you can redistribute it and/or modify it
	under the terms of the GNU General Public License as published by the Free
	Software Foundation; either version 2 of the License, or (at your option)
	any later version.

	This program is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
	for more details.

	You should have received a copy of the GNU General Public License along
	with this program; if not, write to the
	
		Free Software Foundation, Inc.
		51 Franklin Street, Fifth Floor
		Boston, MA  02110-1301, USA.
******************************************************************************/

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline;

namespace Cobalt.Content.Pipeline
{

	[Flags]
	public enum TokenReadFlags
	{
		NoLineBreak = 0x00000001,
		AllowLineBreak = 0x00000002,
		RequireLineStart = 0x00000004,
	}

	public interface IContentTokenizer
	{
		/// <summary>
		/// Parses a token from <paramref name="line"/> starting at <paramref name="index"/> and
		/// advances <paramref name="index"/> to the first position after the token.
		/// </summary>
		/// <param name="line">The line to parse a token from.</param>
		/// <param name="idx">The index to start parsing at.</param>
		/// <returns>The parsed token.</returns>
		string ParseToken( string line, ref int index );
	}

	public sealed class ContentTokenizer : IContentTokenizer
	{

		#region Token Parsing

		private void SkipWhiteSpace( string line, ref int idx )
		{
			while( idx < line.Length && char.IsWhiteSpace( line[idx] ) )
				idx++;
		}

		private string ParseFixedToken( string line, ref int idx, int len )
		{
			string ret = line.Substring( idx, len );
			idx += len;
			return ret;
		}

		private bool IsDigit( char ch )
		{
			return (ch >= '0' && ch <= '9');
		}

		private bool IsNumberChar( char ch )
		{
			return IsDigit( ch ) || ch == '.';
		}

		private bool IsIdentifierChar( char ch )
		{
			return (ch >= '0' && ch <= '9') ||
				IsLeadIdentifierChar( ch );
		}

		private bool IsLeadIdentifierChar( char ch )
		{
			return
				(ch >= 'a' && ch <= 'z') ||
				(ch >= 'A' && ch <= 'Z') ||
				ch == '_';
		}

		private string ParseToken( string line, ref int idx, Predicate<char> isTokenChar )
		{
			int len;
			for( len = 1; idx + len < line.Length; len++ )
			{
				if( !isTokenChar( line[idx + len] ) )
					break;
			}

			return ParseFixedToken( line, ref idx, len );
		}

		private string ParseStringLiteral( string line, ref int idx )
		{
			char quote = line[idx];

			int len;
			for( len = 1; idx + len < line.Length; len++ )
			{
				if( line[idx + len] == quote && line[idx + len - 1] != '\\' )
					break;
			}

			if( idx + len == line.Length )
				throw new InvalidContentException( "Unexpected newline or end of file in string literal." );

			string ret = line.Substring( idx + 1, len - 1 );

			idx += len + 1;
			return ret;
		}

		public string ParseToken( string line, ref int idx )
		{
			SkipWhiteSpace( line, ref idx );

			char ch = line[idx];

			if( IsLeadIdentifierChar( ch ) )
				return ParseToken( line, ref idx, IsIdentifierChar );

			if( IsDigit( ch ) )
				return ParseToken( line, ref idx, IsNumberChar );

			if( ch == '\'' || ch == '\"' )
				return ParseStringLiteral( line, ref idx );

			if( idx + 1 < line.Length )
			{
				//check for 2-character tokens
				char ch2 = line[idx + 1];

				switch( ch )
				{
				case '.':
					if( IsNumberChar( ch2 ) )
						return ParseToken( line, ref idx, IsNumberChar );
					break;

				case '+':
				case '-':
					if( IsNumberChar( ch2 ) )
						return ParseToken( line, ref idx, IsNumberChar );
					goto case '>';

				case '>':
				case '<':
				case '=':
				case '!':
				case '&':
				case '|':
				case '^':
				case '*':
				case '/':
				case '%':
					if( ch2 == '=' )
						return ParseFixedToken( line, ref idx, 2 );
					break;

				case '?':
					if( ch2 == '?' )
						return ParseFixedToken( line, ref idx, 2 );
					break;
				}
			}

			switch( ch )
			{
			case '>':
			case '<':
			case '=':
			case '&':
			case '^':
			case '|':
			case '~':
			case '`':
			case '+':
			case '-':
			case '*':
			case '/':
			case '[':
			case ']':
			case '(':
			case ')':
			case '{':
			case '}':
			case ':':
			case ';':
			case '?':
			case '$':
			case '%':
			case '.':
			case ',':
			case '#':
			case '@':
			case '!':
			case '\\':
				return ParseFixedToken( line, ref idx, 1 );
			}

			throw new InvalidContentException( "Unable to parse token." );
		}

		#endregion
	}

	public sealed class ContentTokenReader : IDisposable
	{
		private ContentLineReader reader;

		public ContentTokenReader( string text, ContentIdentity sourceIdentity, IContentTokenizer tokenizer )
			: this( tokenizer )
		{
			if( text == null )
				throw new ArgumentNullException( "text" );

			reader = new ContentLineReader( text, sourceIdentity );
			tokenizer = new ContentTokenizer();
		}

		public ContentTokenReader( string text, ContentIdentity sourceIdentity )
			: this( new ContentTokenizer() )
		{
			if( text == null )
				throw new ArgumentNullException( "text" );

			reader = new ContentLineReader( text, sourceIdentity );
			tokenizer = new ContentTokenizer();
		}

		public ContentTokenReader( ContentLineReader text, IContentTokenizer tokenizer )
			: this( tokenizer )
		{
			if( text == null )
				throw new ArgumentNullException( "text" );

			reader = text;
		}

		public ContentTokenReader( ContentLineReader text )
			: this( new ContentTokenizer() )
		{
			if( text == null )
				throw new ArgumentNullException( "text" );

			reader = text;
		}

		private ContentTokenReader( IContentTokenizer tokenizer )
		{
			if( tokenizer == null )
				throw new ArgumentNullException( "tokenizer" );
			this.tokenizer = tokenizer;
		}

		public static ContentTokenReader FromFile( string filename, IContentTokenizer tokenizer )
		{
			ContentTokenReader ret = new ContentTokenReader( tokenizer );
			ret.reader = ContentLineReader.FromFile( filename );
			return ret;
		}

		public static ContentTokenReader FromFile( string filename )
		{
			ContentTokenReader ret = new ContentTokenReader( new ContentTokenizer() );
			ret.reader = ContentLineReader.FromFile( filename );
			return ret;
		}

		public void Dispose()
		{
			reader.Dispose();
		}

		public bool HasMoreTokensInCurrentLine
		{
			get { return currTokenIdx < currTokens.Count; }
		}

		public ContentIdentity SourceIdentity
		{
			get { return reader.SourceIdentity; }
		}

		public ContentIdentity LastTokenIdentity
		{
			get { return reader.LastLineIdentity; }
		}

		public string LastToken
		{
			get { return lastToken; }
		}

		private IContentTokenizer tokenizer;
		public IContentTokenizer Tokenizer { get { return tokenizer; } }

		#region InternalReadToken & co

		private int currTokenIdx = 0;
		private List<int> currTokenLocations = new List<int>();
		private List<string> currTokens = new List<string>();
		private string currTokensLine = null;
		private string lastToken;

		private bool TokenizeLine( string line )
		{
			currTokens.Clear();
			currTokenLocations.Clear();

			currTokensLine = line;

			for( int idx = 0; idx < line.Length; )
			{
				string tok;
				int tokIdx = idx;
								
				try
				{
					tok = tokenizer.ParseToken( line, ref idx );
				}
				catch( InvalidContentException ex )
				{
					ex.ContentIdentity = LastTokenIdentity;
					throw ex;
				}

				if( tok != null )
				{
					currTokens.Add( tok );
					currTokenLocations.Add( tokIdx );
				}
			}

			return currTokens.Count > 0;
		}

		/// <summary>
		/// Skips the rest of the current line and tokenizes the next one.
		/// </summary>
		/// <returns>
		/// Returns the next line, or <c>null</c> if there are no more lines,
		/// or if there is a tokenization error.
		/// </returns>
		private string ParseNextLine()
		{
			string line = reader.ReadLine();
			if( line == null )
				return null;

			currTokenIdx = 0;
			if( !TokenizeLine( line ) )
				return null;

			return line;
		}

		public ContentLineReader LineReader { get { return reader; } }

		/// <summary>
		/// Discards the rest of the current line's tokens and
		/// prepares to begin parsing from the underlying reader's
		/// next line.
		/// </summary>
		public void Synchronize()
		{
			currTokens.Clear();
			currTokensLine = null;
			currTokenIdx = 0;
		}

		private string InternalReadToken( TokenReadFlags readFlags )
		{
			TokenReadFlags lineFlags = readFlags &
				(TokenReadFlags.AllowLineBreak |
				TokenReadFlags.NoLineBreak |
				TokenReadFlags.RequireLineStart);

			switch( lineFlags )
			{
			case TokenReadFlags.NoLineBreak:
				break;

			case TokenReadFlags.AllowLineBreak:
				break;

			case TokenReadFlags.RequireLineStart:
				if( HasMoreTokensInCurrentLine )
					throw new InvalidContentException( "Expected to read from start of line, but tokens remain on last line.", reader.LastLineIdentity );
				break;

			default:
				throw new ArgumentException();
			}

			if( currTokenIdx >= currTokens.Count )
			{
				if( lineFlags == TokenReadFlags.NoLineBreak )
					//out of chars, not allowed to parse another line
					return null;

				if( ParseNextLine() == null )
					return null;
			}

			return currTokens[currTokenIdx++];
		}
		#endregion

		#region ReadToEndOfLine
		/// <summary>
		/// Reads to the end of the line.
		/// </summary>
		/// <returns>
		/// The unparsed portion of the current line, or
		/// null if the current line has been fully parsed.
		/// </returns>
		public string ReadToEndOfLine()
		{
			if( currTokenIdx >= currTokens.Count )
				return null;		

			string ret = currTokensLine.Substring( currTokenLocations[currTokenIdx] );
			currTokenIdx = currTokens.Count; //mark all as read

			lastToken = ret;

			return ret;
		}

		/// <summary>
		/// Reads to the end of the line, or if there are no
		/// tokens left on this line it returns the next line.
		/// </summary>
		/// <returns></returns>
		public string ReadToEndOfLineOrNext()
		{
			string curLine = ReadToEndOfLine();

			if( curLine == null )
			{
				if( ParseNextLine() == null )
					return null;

				curLine = ReadToEndOfLine();
			}

			return curLine;
		}
		#endregion

		#region ReadToken
		public string ReadToken( TokenReadFlags readFlags )
		{
			lastToken = InternalReadToken( readFlags );
			return lastToken;
		}

		public string ReadToken()
		{
			return ReadToken( TokenReadFlags.AllowLineBreak );
		}
		#endregion

		#region ReadRequiredToken
		public string ReadRequiredToken( TokenReadFlags readFlags )
		{
			string token = ReadToken( readFlags );

			if( token == null )
				ThrowInvalidEndOfFile();

			return token;
		}

		public string ReadRequiredToken()
		{
			return ReadRequiredToken( TokenReadFlags.AllowLineBreak );
		}
		#endregion

		#region ReadExactToken
		public string ReadExactToken( string expectedToken, StringComparison cmp, TokenReadFlags readFlags )
		{
			string token = ReadToken( readFlags );

			if( token == null )
			{
				if( expectedToken != null )
					ThrowInvalidEndOfFile( expectedToken );

				return null;
			}

			if( string.Compare( token, expectedToken, cmp ) != 0 )
				ThrowInvalidToken( expectedToken );

			return token;
		}

		public string ReadExactToken( string expectedToken, TokenReadFlags readFlags )
		{
			return ReadExactToken( expectedToken, StringComparison.Ordinal, readFlags );
		}

		public string ReadExactToken( string expectedToken, StringComparison cmp )
		{
			return ReadExactToken( expectedToken, cmp, TokenReadFlags.AllowLineBreak );
		}

		public string ReadExactToken( string expectedToken )
		{
			return ReadExactToken( expectedToken, StringComparison.Ordinal, TokenReadFlags.AllowLineBreak );
		}
		#endregion

		#region Throw Parse Error
		public void ThrowInvalidEndOfFile( string expected )
		{
			throw new InvalidContentException(
				string.Format( "Unexpected end of file, expected '{0}'.", expected ),
				LastTokenIdentity );
		}

		public void ThrowInvalidEndOfFile()
		{
			throw new InvalidContentException( "Unexpected end of file.", LastTokenIdentity );
		}

		public void ThrowInvalidToken( string expected )
		{
			throw new InvalidContentException(
				string.Format( "Invalid token '{0}', expected '{1}'.", LastToken, expected ),
				LastTokenIdentity );
		}

		public void ThrowInvalidToken()
		{
			throw new InvalidContentException(
				string.Format( "Invalid token '{0}'.", LastToken ),
				LastTokenIdentity );
		}
		#endregion
	}

}